﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Sims3Package.Interface;

namespace Sims3Package
{
    /// <summary>
    /// Sims 3 Package File
    /// </summary>
    public class Package : IPackage, IDisposable
    {
        // Properties
        /// <summary>
        /// Major number, 2 in Sims 3 package.
        /// </summary>
        public int Major { get; set; }
        /// <summary>
        /// Minor number, 0 in Sims 3 package
        /// </summary>
        public int Minor { get; set; }

        /// <summary>
        /// Unknown 1, length 24 byte array
        /// </summary>
        public byte[] Unknown1 { get; set; }
        /// <summary>
        /// Number of index entries - if zero, size and position also zero
        /// </summary>
        public int EntryCount { get { return this.EntryList.Count; } }
        /// <summary>
        /// Unknown 2, length 4 byte array
        /// </summary>
        public uint Unknown2 { get; set; }
        /// <summary>
        /// Size of index on disk in bytes
        /// </summary>
        protected internal int IndexSize { get; set; }
        /// <summary>
        /// Unknown 3, length 12 byte array
        /// </summary>
        public byte[] Unknown3 { get; set; }
        /// <summary>
        /// Index version, always 3
        /// </summary>
        public int IndexVersion { get; set; }
        /// <summary>
        /// Absolute position of index.
        /// </summary>
        public int IndexPosition { get; set; }
        /// <summary>
        /// Unknown 4, length 28 byte array
        /// </summary>
        public byte[] Unknown4 { get; set; }
        /// <summary>
        /// List of entries in package
        /// </summary>
        public List<ResourceEntry> EntryList { get; set; }
        

        // Private properties
        
        /// <summary>
        /// Magic bytes, i.e. "DBPF".
        /// </summary>
        private static readonly int Magic = 0x46504244;
        /// <summary>
        /// Contains the stream of pack file
        /// </summary>
        private Stream PackStream = null;
        private bool isDisposed = false;

        /// <summary>
        /// Create a new package file.
        /// </summary>
        public Package()
        {
            // Header File
            this.Major = 2;
            this.Minor = 0;
            this.Unknown1 = new byte[24];
            this.Unknown2 = 0;
            this.Unknown3 = new byte[12];
            this.IndexVersion = 3;
            this.Unknown4 = new byte[28];

            this.EntryList = new List<ResourceEntry>();
        }
        /// <summary>
        /// Reads a package in read-only way
        /// </summary>
        /// <param name="path">The string path of package file</param>
        public Package(string path)
            : this(path, false)
        {
        }
        /// <summary>
        /// Reads a package with given path
        /// </summary>
        /// <param name="path">The string path of package file</param>
        /// <param name="readwrite">If true, reads package with Read-Write rights, otherwise read-only</param>
        public Package(string path, bool readwrite)
            : this(new FileStream(path, FileMode.Open, readwrite ? FileAccess.ReadWrite : FileAccess.Read, readwrite? FileShare.None: FileShare.Read))
        { }
        /// <summary>
        /// Read package from a stream
        /// </summary>
        /// <param name="s">Stream containing package data</param>
        public Package(Stream s)
        {
            this.PackStream = s;
            BinaryReader r = new BinaryReader(PackStream);

            // Header File
            int magic = r.ReadInt32();
            if (!magic.Equals(Magic) || PackStream.Length < 96) throw new InvalidDataException("Not a correct DBPF file");
            this.Major = r.ReadInt32();
            this.Minor = r.ReadInt32();
            this.Unknown1 = r.ReadBytes(24);
            int entryCount = r.ReadInt32();
            this.Unknown2 = r.ReadUInt32();
            this.IndexSize = r.ReadInt32();
            this.Unknown3 = r.ReadBytes(12);
            this.IndexVersion = r.ReadInt32();
            this.IndexPosition = r.ReadInt32();
            this.Unknown4 = r.ReadBytes(28);

            // Entries
            this.EntryList = new List<ResourceEntry>(entryCount);
            if (entryCount > 0 && this.IndexPosition != 0)
            {

                PackStream.Position = this.IndexPosition;
                int indextype = r.ReadInt32();

                Int32[] hdr = new Int32[Hdrsize(indextype)];
                Int32[] entry = new Int32[9 - Hdrsize(indextype)]; // Since index type is counted in hdrsize, use 9 instead

                hdr[0] = indextype;

                for (int i = 1; i < hdr.Length; i++)
                    hdr[i] = r.ReadInt32();

                for (int i = 0; i < entryCount; i++)
                {
                    for (int j = 0; j < entry.Length; j++)
                        entry[j] = r.ReadInt32();
                    this.EntryList.Add(new ResourceEntry(hdr, entry));
                }


            }
        }
        /// <summary>
        /// Save the package to where it is
        /// </summary>
        public void Save()
        {
            if (PackStream == null) throw new Exception("No data to save, if it's the first time to be saved, use Save As instead");
            if (!PackStream.CanWrite) throw new InvalidOperationException("Package is read-only");
            FileStream fs = PackStream as FileStream;
            string tempFile = Path.GetTempFileName();
            try
            {
                SaveAs(tempFile);
                using (FileStream s = new FileStream(tempFile, FileMode.Open, FileAccess.Read))
                {
                    fs.Position = 0;
                    s.CopyTo(fs);
                }
            }
            finally
            {
                File.Delete(tempFile);
            }

            //PackStream = fs;
            
        }
        
        /// <summary>
        /// Save package to given path
        /// </summary>
        /// <param name="path"></param>
        public void SaveAs(string path)
        {

            using (FileStream s = new FileStream(path, FileMode.Create, FileAccess.ReadWrite))
            {
                //FileStream s = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
                BinaryWriter w = new BinaryWriter(s);

                // Write header---96 bytes
                w.Write(Magic);
                w.Write(this.Major);
                w.Write(this.Minor);
                w.Write(this.Unknown1);
                w.Write(this.EntryCount);
                w.Write(this.Unknown2);


                w.Write(GetIndexBytes().Length);
                w.Write(this.Unknown3);
                w.Write(this.IndexVersion);

                w.Flush();

                Int32 size = 0;
                bool empty = this.EntryList == null || this.EntryList.Count == 0;



                if (!empty)
                {
                    foreach (ResourceEntry index in this.EntryList)
                    {

                        if (index.Compressed)
                        {
                            if (index.FileSize == index.Memsize)
                            {
                                if (index.FileSize != 0)
                                {
                                    Int32 filesize = Compression.GetFilesize(GetResourceStream(index));
                                    //byte[] data2 = new byte[filesize];
                                    //Stream testStream = GetResourceStream(index);
                                    //testStream.Position = 0;
                                    //GetResourceStream(index).Read(data2, 0, data2.Length);
                                    index.realsize = filesize;
                                }
                                else { index.realsize = 0; index.mCompressed = false; }
                            }
                            else
                            {
                                index.realsize = index.FileSize;
                            }

                            size += index.realsize;
                        }
                        else
                        {
                            size += index.Memsize;
                            index.realsize = index.Memsize;
                        }

                    }
                }


                w.Write(size + 96); // Write index position

                w.Write(this.Unknown4);

                if (!empty)
                {
                    // Entry data block
                    foreach (ResourceEntry entry in this.EntryList)
                    {

                        if (entry.FileSize > 0)
                        {


                            byte[] data = new byte[entry.realsize];
                            //byte[] data = new byte[entry.Memsize];
                            entry.Compressed = data.Length != entry.Memsize;
                            Stream s2 = GetResourceStream(entry);
                            s2.Position = 0;
                            if (!entry.Compressed) { s2.Read(data, 0, data.Length); } else { Compression.Compress(s2).Read(data, 0, data.Length); }
                            //stream.Read(data, 0, data.Length);


                            entry.Chunkoffset = (Int32)s.Position;

                            w.Write(data);
                            entry.mFilesize = entry.realsize;

                        }
                        else
                        {
                            entry.Chunkoffset = (Int32)s.Position;
                            entry.Compressed = false;
                            entry.realsize = 0;
                        }

                    }

                    w.Flush();

                    if (s.Position != size + 96)
                        throw new InvalidDataException("Position is not correct");


                    // Write indexes
                    //int type = 0;
                    //List<UInt32> header = new List<UInt32>(4);
                    //if (this.EntryList.All<ITGIblock>(entry => entry.ResourceType == this.EntryList[0].ResourceType))
                    //{ type |= 1; header.Add(this.EntryList[0].ResourceType); }
                    //if (this.EntryList.All<ITGIblock>(entry => entry.ResourceGroup == this.EntryList[0].ResourceGroup))
                    //{ type |= 1 << 1; header.Add(this.EntryList[0].ResourceGroup); }
                    //if (this.EntryList.All<ITGIblock>(entry => (uint)(entry.ResourceInstance >> 32) == this.EntryList[0].InstanceHi))
                    //{ type |= 1 << 2; header.Add(this.EntryList[0].InstanceHi); }
                    //if (this.EntryList.All<ITGIblock>(entry => (uint)(entry.ResourceInstance & 0x00000000FFFFFFFF) == this.EntryList[0].InstanceLo))
                    //{ type |= 1 << 3; header.Add(this.EntryList[0].InstanceLo); }


                    //w.Write(type);

                    //// Write Headers
                    //if (type != 0 && header.Count != 0) foreach (uint i in header) w.Write(i);
                    //// Write index blocks
                    //foreach (IPackageIndex index in this.EntryList)
                    //    w.Write(index.GetIndexBytes(type));

                    w.Write(GetIndexBytes());

                }
                else
                {
                    w.Write((int)0);
                }

                
                //s.Close();
                
            }



        }

        #region Test
#if false
        public uint getmesize(ResourceEntry entry)
        {
            return Compression.GetMemsize(GetResourceStream(entry));
        }
        

#endif
        #endregion
        #region Functions
        /// <summary>
        /// Get stream of certain TGi. If certain TGI is not contained in package, null is returned.
        /// </summary>
        /// <param name="block">TGI Block</param>
        /// <returns></returns>
        public Stream GetResourceStream(ITGIblock block)
        {
            if (block == null) return null;
            ResourceEntry entry = FindTGI(block);
            if (entry != null)
            {
                if (entry.ResourceStream != null)
                {
                    entry.ResourceStream.Position = 0;
                    return entry.ResourceStream;
                }
                BinaryReader r = new BinaryReader(PackStream);
                r.BaseStream.Position = entry.Chunkoffset;
                byte[] data = r.ReadBytes((int)entry.FileSize);

                Stream s = (entry.Memsize != entry.FileSize) ? Compression.Decompress(data) : new MemoryStream(data);
                s.Position = 0;
                return s;
            }
            return null;
        }

        private byte[] GetResourceBytesFromDisk(ITGIblock block)
        {
            if (block == null) return null;
            ResourceEntry entry = FindTGI(block);
            if (entry != null)
            {
                if (entry.Chunkoffset == Int32.MaxValue && entry.ResourceStream != null) return null;    // It shouldn't encounter this problem when used carefully
                BinaryReader r = new BinaryReader(PackStream);
                r.BaseStream.Position = entry.Chunkoffset;
                byte[] data = r.ReadBytes((int)entry.FileSize);
                return data;
            }
            return null;
        }

        private byte[] GetIndexBytes()
        {
            using (MemoryStream s = new MemoryStream())
            {
                BinaryWriter w = new BinaryWriter(s);
                int type = 0;
                if (this.EntryList.Count > 0)
                {
                    List<UInt32> header = new List<UInt32>(4);
                    if (this.EntryList.All<ITGIblock>(entry => entry.ResourceType == this.EntryList[0].ResourceType))
                    { type |= 1; header.Add(this.EntryList[0].ResourceType); }
                    if (this.EntryList.All<ITGIblock>(entry => entry.ResourceGroup == this.EntryList[0].ResourceGroup))
                    { type |= 1 << 1; header.Add(this.EntryList[0].ResourceGroup); }
                    if (this.EntryList.All<ITGIblock>(entry => (uint)(entry.ResourceInstance >> 32) == this.EntryList[0].InstanceHi))
                    { type |= 1 << 2; header.Add(this.EntryList[0].InstanceHi); }
                    //  No use. The game never use for Instance Lo according to my test, and make the type same same with s3pe's
                    //if (this.EntryList.All<ITGIblock>(entry => (uint)(entry.ResourceInstance & 0x00000000FFFFFFFF) == this.EntryList[0].InstanceLo))
                    //{ type |= 1 << 3; header.Add(this.EntryList[0].InstanceLo); } 

                    

                    w.Write(type);
                    // Write Headers
                    if (type != 0 && header.Count != 0) foreach (uint i in header) w.Write(i);
                    // Write index blocks
                    foreach (IPackageIndex index in this.EntryList)
                        w.Write(index.GetIndexBytes(type));
                }
                else if (this.EntryList.Count == 0) { w.Write(7); w.Write(0); w.Write(0); }

                

                w.Flush();

                return s.ToArray();
            }
        }
       
        /// <summary>
        /// Replace the certain stream with given TGI in compression.
        /// </summary>
        /// <param name="block">TGI Block</param>
        /// <param name="s">Stream replaced by</param>
        public void ReplaceResource(ITGIblock block, Stream s)
        {
            ReplaceResource(block, s, true);
        }
        /// <summary>
        /// Replace the certain stream with given TGI
        /// </summary>
        /// <param name="block">TGI Block</param>
        /// <param name="s">Stream replaced by</param>
        /// <param name="compress">Whether to compress resource stream. True is recommended</param>
        public void ReplaceResource(ITGIblock block, Stream s, bool compress)
        {
            ResourceEntry entry = FindTGI(block);
            if (entry != null)
            {
                entry.Compressed = compress;
                entry.ResourceStream = s;
                entry.Chunkoffset = int.MaxValue;
            }
        }
        /// <summary>
        /// Add a resource entry
        /// </summary>
        /// <param name="entry"></param>
        public void AddResourceEntry(ResourceEntry entry)
        {
            this.EntryList.Add(entry);
        }
        /// <summary>
        /// Add a resource entry with given TGI and stream in compression
        /// </summary>
        /// <param name="block">TGI block</param>
        /// <param name="s">Resource stream</param>
        public void AddResourceEntry(ITGIblock block, Stream s)
        {
            AddResourceEntry(block, s, true);
        }
        /// <summary>
        /// Add resource with given TGI and stream
        /// </summary>
        /// <param name="block">TGI block</param>
        /// <param name="s">Resource stream</param>
        /// <param name="compress">Whether to compress or not</param>
        /// <remarks>It will add the resource directly to package regardless of duplication</remarks>
        public void AddResourceEntry(ITGIblock block, Stream s, bool compress)
        {
            this.EntryList.Add(new ResourceEntry(block, s, compress));
        }
        /// <summary>
        /// Add resource with given TGI and stream
        /// </summary>
        /// <param name="block">TGI block</param>
        /// <param name="s">Resource stream</param>
        /// <param name="compress">Whether to compress or not</param>
        /// <param name="replace">True to replace duplicated package; otherwise reject replacing</param>
        public void AddResourceEntry(ITGIblock block, Stream s, bool compress, bool replace)
        {
            if (TGIExist(block) && replace) this.ReplaceResource(FindTGI(block), s, compress);
            else if (TGIExist(block) && !replace) return;
            this.EntryList.Add(new ResourceEntry(block, s, compress));
        }
        /// <summary>
        /// Delete resource from package. Nothing happens though if it doesn't exit.
        /// </summary>
        /// <param name="block">TGI Block</param>
        public void DeleteResource(ITGIblock block)
        {
            ResourceEntry entry = FindTGI(block);
            if (entry != null) { entry.Dispose(); this.EntryList.Remove(entry); }
        }

      

        /// <summary>
        /// Find resource entry with given TGI
        /// </summary>
        /// <param name="block">Block containing TGI information</param>
        /// <returns>Entry with same TGI information with block; null if not found</returns>
        private ResourceEntry FindTGI(ITGIblock block)
        {
            if(TGIExist(block)) {return this.EntryList.Find(tgi => tgi.ResourceInstance == block.ResourceInstance && block.ResourceGroup == tgi.ResourceGroup
                && tgi.ResourceType == block.ResourceType); }
            else {return null;}
        }

        private bool TGIExist(ITGIblock block)
        {
            return this.EntryList.Exists(tgi => tgi.ResourceInstance == block.ResourceInstance && block.ResourceGroup == tgi.ResourceGroup
                && tgi.ResourceType == block.ResourceType);
        }

        
        private static int Hdrsize(int indextype)
        {
            int hc = 1;
            for (int i = 0; i < sizeof(uint); i++) if ((indextype & (1 << i)) != 0) hc++;
            return hc;
        }
        /// <summary>
        /// Clean up resource
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.isDisposed)
            {
                if (disposing)
                {
                    PackStream.Close();
                    PackStream = null;
                    foreach (ResourceEntry entry in this.EntryList)
                        entry.Dispose();
                    this.EntryList = null;
                }
                this.isDisposed = true;
            }
        }

        #endregion


    }
}
